/*
 * Copyright 2021-2022 the original author(https://github.com/wj596)
 * 
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * </p>
 */
package org.jsets.fastboot.security;

import org.jsets.fastboot.util.CollectionUtils;
import org.jsets.fastboot.util.StringUtils;
import org.jsets.fastboot.util.WebUtils;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.jsets.fastboot.security.authc.AuthInfo;
import org.jsets.fastboot.security.authc.AuthcRequest;
import org.jsets.fastboot.security.authc.AuthcResult;
import org.jsets.fastboot.security.authc.IAuthenticator;
import org.jsets.fastboot.security.authc.LoginHandlerManager;
import org.jsets.fastboot.security.authc.PasswdRetryWatcher;
import org.jsets.fastboot.security.authz.AuthzRuleManager;
import org.jsets.fastboot.security.authz.IAuthorizer;
import org.jsets.fastboot.security.cache.ICacheManager;
import org.jsets.fastboot.security.captcha.Base64CaptchaResult;
import org.jsets.fastboot.security.config.SecurityProperties;
import org.jsets.fastboot.security.listener.IListenerManager;
import org.jsets.fastboot.security.online.OnlineManager;
import org.jsets.fastboot.security.rememberme.RememberMeManager;
import org.jsets.fastboot.security.token.ITokenManager;
import org.jsets.fastboot.security.token.TokenStub;
import com.google.common.io.BaseEncoding;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;


/**
 * 安全工具类
 *
 * @Author wangjie
 * @date 2021.07.05 22:04
 */
@Slf4j
public class SecurityUtils {

	private static SecurityProperties properties;
	private static ICacheManager cacheManager;
	private static ITokenManager tokenManager;
	private static IListenerManager listenerManager;
	private static RememberMeManager rememberMeManager;
	private static OnlineManager onlineManager;
	private static AuthzRuleManager authzRuleManager;
	private static LoginHandlerManager loginHandlerManager;
	private static IAuthenticator authenticator;
	private static IAuthorizer authorizer;
	private static PasswordEncoder passwordEncoder;
	private static IUserInfoProvider userInfoProvider;
	private static ICaptchaProvider captchaProvider;
	private static PasswdRetryWatcher passwdRetryWatcher;

	public static Base64CaptchaResult getCaptcha() {
		return getCaptchaProvider().generateBase64Captcha();
	}
	
	public static void validateAccessToken(String token) {
		getAuthenticator().authenticate(token);
	}
	
	public static AuthcResult login(AuthcRequest authcRequest) throws AuthenticationException {
		return getAuthenticator().login(authcRequest);
	}
	
	public static AuthcResult directLogin(AuthcRequest authcRequest) throws AuthenticationException {
		AuthcResult result = getTokenManager().issueAccessToken(authcRequest);
		getListenerManager().onLoginSuccess(authcRequest, result);
		return result;
	}
	
	/**
	 * 认证
	 */
	public static void authenticate(String token) {
		getAuthenticator().authenticate(token);
	}
	
	/**
	 * 登出
	 */
	public static void logout(HttpServletRequest servletRequest) {
		getAuthenticator().logout(servletRequest);
	}
	
	/**
	 * 当前用户是否认证
	 */
	public static boolean isAuthenticated() {
		AuthInfo authInfo = SecurityContext.get();
		if (Objects.isNull(authInfo)) {
			return false;
		}
		return true;
	}
	
	public static String getCurrentToken() {
		AuthInfo authInfo = SecurityContext.get();
		if (Objects.isNull(authInfo)) {
			throw new InsufficientAuthenticationException(getProperties().getUnauthorizedTips());
		}
		return authInfo.getAccessToken();
	}
	
	/**
	 * 获取当前上下文中的Username
	 */
	public static String getCurrentUsername() {
		AuthInfo authInfo = SecurityContext.get();
		if (Objects.isNull(authInfo)) {
			throw new InsufficientAuthenticationException(getProperties().getUnauthorizedTips());
		}
		return authInfo.getUsername();
	}
	
	/**
	 * 获取当前认证的用户
	 */
	public static <T extends UserDetails> T getAccount() {
		AuthInfo authInfo = SecurityContext.get();
		if (Objects.isNull(authInfo)) {
			throw new InsufficientAuthenticationException(getProperties().getUnauthorizedTips());
		}

		UserDetails account = getUserInfoProvider().loadUserByUsername(authInfo.getAccessToken(), authInfo.getUsername());
		if (Objects.isNull(account)) {
			throw new InsufficientAuthenticationException(getProperties().getUnauthorizedTips());
		}

		return (T) account;
	}
	
	/**
	 * 获取当前认证信息 
	 */
	public static AuthInfo getAuthInfo() {
		AuthInfo authInfo = SecurityContext.get();
		if (Objects.isNull(authInfo)) {
			throw new InsufficientAuthenticationException(getProperties().getUnauthorizedTips());
		}
		return authInfo;
	}

	/**
	 * 删除当前安全上下文中的认证信息
	 */
	public static void cleanupAuthInfo() {
		SecurityContext.cleanup();
	}

	public static void cleanupPasswdRetryRecord(String username) {
		getPasswdRetryWatcher().cleanup(username);
	}
	
	/**
	 * 加密明文密码
	 * 
	 * @param plain
	 * @return
	 */
	public static String encryptPassword(String plain) {
		return getPasswordEncoder().encode(plain);
	}

	public static void refreshAuthzRules() {
		getAuthzRuleManager().refreshAuthzRules();
	}

	public static String issueJWT(String id, String subject, String secretKey) {
		String jwt = Jwts.builder().
				setId(id).
				setSubject(subject).
				signWith(SignatureAlgorithm.HS256, secretKey.getBytes()).
				compact();
		return BaseEncoding.base64Url().encode(jwt.getBytes());
	}

	public static String issueJWT(String id, String subject, String secretKey, Long expireTime) {
		Date expiresDate = new Date(System.currentTimeMillis() + expireTime * 1000);
		String jwt = Jwts.builder().
				setId(id).
				setSubject(subject).
				setExpiration(expiresDate).
				signWith(SignatureAlgorithm.HS256, secretKey.getBytes()).
				compact();
		return BaseEncoding.base64Url().encode(jwt.getBytes());
	}

	public static String issueJWT(String id, String subject, String secretKey, Long expireTime, Map<String, Object> claims) {
		Date expiresDate = new Date(System.currentTimeMillis() + expireTime * 1000);
		String jwt = Jwts.builder().
				setId(id).
				setSubject(subject).
				addClaims(claims).
				setExpiration(expiresDate).
				signWith(SignatureAlgorithm.HS256, secretKey.getBytes()).
				compact();
		return BaseEncoding.base64Url().encode(jwt.getBytes());
	}

	public static TokenStub validateJWT(String jwt) throws AuthenticationException{
		String signKey = getProperties().getTokenSignKey();
		Claims claims = null;
		try {
			String content = new String(BaseEncoding.base64Url().decode(jwt));
			claims = Jwts.parser().setSigningKey(signKey.getBytes()).parseClaimsJws(content).getBody();
		} catch (ExpiredJwtException e) {
			log.error(e.getMessage(), e);
			throw new InsufficientAuthenticationException(getProperties().getTokenExpiredTips());
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			throw new InsufficientAuthenticationException(getProperties().getTokenInvalidTips());
		}
		if (Objects.isNull(claims)) {
			throw new InsufficientAuthenticationException(getProperties().getTokenInvalidTips());
		}
		
		TokenStub stub = new TokenStub();
		try {
			String content = new String(BaseEncoding.base64Url().decode(jwt));
			claims = Jwts.parser().setSigningKey(signKey.getBytes()).parseClaimsJws(content).getBody();
			stub.setId(claims.getId());
			stub.setUsername(claims.getSubject());
		} catch (ExpiredJwtException e) {
			log.error(e.getMessage(), e);
			throw new InsufficientAuthenticationException(getProperties().getTokenInvalidTips());
		}
		log.info("验证JWT[{}]成功", jwt);
		return stub;
	}

	/**
	 * HMAC签名
	 * 
	 * @param timestamp 时间戳
	 * @return String
	 */
	public static String hmacSignature(Long timestamp) {
		String context = timestamp.toString();
		if (StringUtils.notEmpty(getProperties().getHmacSignSalt())) {
			context = context + getProperties().getHmacSignSalt();
		}
		HMac mac = new HMac(HmacAlgorithm.HmacSHA256, getProperties().getHmacSignKey().getBytes());
		return mac.digestBase64(context, true);
	}

	/**
	 * HMAC签名验证
	 * 
	 * @param timestamp 时间戳
	 * @param sign      签名
	 * @return
	 */
	public static boolean hmacSignatureValidate(Long timestamp, String sign) {
		String context = timestamp.toString();
		if (StringUtils.notEmpty(getProperties().getHmacSignSalt())) {
			context = context + getProperties().getHmacSignSalt();
		}

		HMac mac = new HMac(HmacAlgorithm.HmacSHA256, getProperties().getHmacSignKey().getBytes());
		String curr = mac.digestBase64(context, true);
		if (!curr.equals(sign)) {
			return false;
		}

		long ss = System.currentTimeMillis() - timestamp;
		if (ss > (getProperties().getHmacSignTimeout() * 1000)) {
			log.warn("令牌过期");
			return false;
		}
		return true;
	}

	public static SecurityProperties getProperties() {
		return properties;
	}

	public static void setProperties(SecurityProperties properties) {
		if (null != SecurityUtils.properties) {
			return;
		}
		SecurityUtils.properties = properties;
	}
	
	public static ICacheManager getCacheManager() {
		return cacheManager;
	}

	public static void setCacheManager(ICacheManager cacheManager) {
		if (null != SecurityUtils.cacheManager) {
			return;
		}
		SecurityUtils.cacheManager = cacheManager;
	}
	
	public static ITokenManager getTokenManager() {
		return tokenManager;
	}

	public static void setTokenManager(ITokenManager tokenManager) {
		if (null != SecurityUtils.tokenManager) {
			return;
		}
		SecurityUtils.tokenManager = tokenManager;
	}

	public static RememberMeManager getRememberMeManager() {
		return rememberMeManager;
	}

	public static void setRememberMeManager(RememberMeManager rememberMeManager) {
		if (null != SecurityUtils.rememberMeManager) {
			return;
		}
		SecurityUtils.rememberMeManager = rememberMeManager;
	}

	public static OnlineManager getOnlineManager() {
		return onlineManager;
	}

	public static void setOnlineManager(OnlineManager onlineManager) {
		if (null != SecurityUtils.onlineManager) {
			return;
		}
		SecurityUtils.onlineManager = onlineManager;
	}

	public static IListenerManager getListenerManager() {
		return listenerManager;
	}

	public static void setListenerManager(IListenerManager listenerManager) {
		if (null != SecurityUtils.listenerManager) {
			return;
		}
		SecurityUtils.listenerManager = listenerManager;
	}

	public static PasswordEncoder getPasswordEncoder() {
		return passwordEncoder;
	}

	public static void setPasswordEncoder(PasswordEncoder passwordEncoder) {
		if (null != SecurityUtils.passwordEncoder) {
			return;
		}
		SecurityUtils.passwordEncoder = passwordEncoder;
	}

	public static IUserInfoProvider getUserInfoProvider() {
		return userInfoProvider;
	}

	public static void setUserInfoProvider(IUserInfoProvider userInfoProvider) {
		if (null != SecurityUtils.userInfoProvider) {
			return;
		}
		SecurityUtils.userInfoProvider = userInfoProvider;
	}

	public static ICaptchaProvider getCaptchaProvider() {
		return captchaProvider;
	}

	public static void setCaptchaProvider(ICaptchaProvider captchaProvider) {
		if (null != SecurityUtils.captchaProvider) {
			return;
		}
		SecurityUtils.captchaProvider = captchaProvider;
	}

	public static PasswdRetryWatcher getPasswdRetryWatcher() {
		return passwdRetryWatcher;
	}

	public static void setPasswdRetryWatcher(PasswdRetryWatcher passwdRetryWatcher) {
		if (null != SecurityUtils.passwdRetryWatcher) {
			return;
		}
		SecurityUtils.passwdRetryWatcher = passwdRetryWatcher;
	}

	public static IAuthorizer getAuthorizer() {
		return authorizer;
	}

	public static void setAuthorizer(IAuthorizer authorizer) {
		if (null != SecurityUtils.authorizer) {
			return;
		}
		SecurityUtils.authorizer = authorizer;
	}

	public static LoginHandlerManager getLoginHandlerManager() {
		return loginHandlerManager;
	}

	public static void setLoginHandlerManager(LoginHandlerManager loginHandlerManager) {
		if (null != SecurityUtils.loginHandlerManager) {
			return;
		}
		SecurityUtils.loginHandlerManager = loginHandlerManager;
	}
	
	public static IAuthenticator getAuthenticator() {
		return authenticator;
	}
	
	public static void setAuthenticator(IAuthenticator authenticator) {
		if (null != SecurityUtils.authenticator) {
			return;
		}
		SecurityUtils.authenticator = authenticator;
	}

	public static AuthzRuleManager getAuthzRuleManager() {
		return authzRuleManager;
	}

	public static void setAuthzRuleManager(AuthzRuleManager authzRuleManager) {
		if (null != SecurityUtils.authzRuleManager) {
			return;
		}
		SecurityUtils.authzRuleManager = authzRuleManager;
	}
	
	public static boolean isIgnoringPath(String servletPath) {
		if(CollectionUtils.isEmpty(getProperties().getAuthIgnorings())) {
			return false;
		}
		
		for (String s : getProperties().getAuthIgnorings()) {
			if (WebUtils.pathMatch(s, servletPath)) {
				return true;
			}
		}
		return false;
	}
	
}